package com.zjuee.web.basic.captcha.utils;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.zjuee.base.bean.response.CommonResultCode;
import com.zjuee.base.exception.BaseException;
import com.zjuee.base.utils.FileUtil;
import com.zjuee.core.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author wu liang
 * @since 1.0
 */
@Component
@Slf4j
public class CaptchaUtil {

    // 验证码失效时间 5分钟
    public static final long SESSION_CAPT_EXPIRES_TIMES = 5 * 60 * 1000;

    // 用户登录失败 标记前缀
    private static final String FAIL_COUNT_PREFIX = "LoginFailCount:";
    private static RedisTemplate<String,Long> redisTemplate;
    @Resource
    public void setRedisTemplate(RedisTemplate redisTemplate) {
        CaptchaUtil.redisTemplate = redisTemplate;
    }

    // 错误多少次 将要验证码登录
    private static Integer showCaptchaCount;
    @Value("${myApp.captcha.showCaptchaCount:5}")
    private void setShowCaptchaCount(Integer showCaptchaCount){
        CaptchaUtil.showCaptchaCount = showCaptchaCount;
    }

    // 错误多少次 将被锁定账号
    private static Integer lockCount;
    @Value("${myApp.captcha.lockCount:5}")
    private void setLockCount(Integer lockCount){
        if (lockCount == null || lockCount < 1)
            throw new BaseException(CommonResultCode.USER_LOGIN_EXP, "myApp.captcha.lockCount 配置错误");
        CaptchaUtil.lockCount = lockCount;
    }
    // 锁定账号时 锁定多长时间 (分钟)
    private static Integer lockDuration;
    @Value("${myApp.captcha.lockDuration:5}")
    private void setLockDuration(Integer lockDuration){
        if (lockDuration == null || lockDuration < 1)
            throw new BaseException(CommonResultCode.USER_LOGIN_EXP, "myApp.captcha.lockDuration 配置错误");
        CaptchaUtil.lockDuration = lockDuration;
    }
    public static Integer getLockDuration(){
        return CaptchaUtil.lockDuration;
    }

    // 临时校验验证码结果的session key
    public static final String CAPTCHA_SESSION_TEMP_KEY = "CAPTCHA_SESSION_TEMP_KEY";
    // 验证码 存放于session中的key
    public static final String CAPTCHA_SESSION_KEY = "CAPTCHA_SESSION_KEY";

    private static final Map<String, String> slidingOriginalCacheMap = new HashMap<>();  //滑块底图
    private static final Map<String, String> slidingBlockCacheMap = new HashMap(); //滑块
    private static final Map<String, String> picClickCacheMap = new HashMap(); //点选文字

    private static String[] slidingOriginalCacheNames;
    private static String[] slidingBlockCacheNames;
    private static String[] picClickCacheNames;

    // 滑动验证码 图片存放dir
    @Value("${myApp.captcha.images.slideFileDir:captcha/defaultImages/slide}")
    private String slideFileDir;
    // 点选验证码 图片存放dir
    @Value("${myApp.captcha.images.clickWordFileDir:captcha/defaultImages/clickWord}")
    private String clickWordFileDir;
    @PostConstruct
    private void init(){
        // 加载滑动验证码图片
        CaptchaUtil.cacheBootImageSlide(
                CaptchaUtil.getResourcesImagesFile(slideFileDir + "/original"),
                CaptchaUtil.getResourcesImagesFile(slideFileDir + "/slidingBlock")
        );
        // 加载点选验证码
        CaptchaUtil.cacheBootImageClickWord(CaptchaUtil.getResourcesImagesFile(clickWordFileDir));
    }

    /**
     * 初始化 读取原图背景图
     */
    public static void cacheBootImageSlide(Map<String, String> slidingOriginalMap, Map<String, String> slidingBlockMap) {
        if (slidingOriginalMap == null || slidingOriginalMap.isEmpty())
            throw new BaseException(CommonResultCode.USER_LOGIN_EXP, "滑动验证码 原图为空");
        if (slidingBlockMap == null || slidingBlockMap.isEmpty())
            throw new BaseException(CommonResultCode.USER_LOGIN_EXP, "滑动验证码 抠图模型为空");

        slidingOriginalCacheMap.putAll(slidingOriginalMap);
        slidingBlockCacheMap.putAll(slidingBlockMap);

        slidingOriginalCacheNames = slidingOriginalCacheMap.keySet().toArray(new String[0]);
        slidingBlockCacheNames = slidingBlockCacheMap.keySet().toArray(new String[0]);
    }
    public static void cacheBootImageClickWord(Map<String, String> picClickMap) {
        if (picClickMap == null || picClickMap.isEmpty())
            throw new BaseException(CommonResultCode.USER_LOGIN_EXP, "点选验证码 背景图为空");
        picClickCacheMap.putAll(picClickMap);
        picClickCacheNames = picClickCacheMap.keySet().toArray(new String[0]);
    }

    /**
     * 随机获取一张滑动验证码的原图
     */
    public static BufferedImage randomSlidingOriginal() {
        if (null == slidingOriginalCacheNames || slidingOriginalCacheNames.length == 0) {
            return null;
        }

        int randomInt = new Random().nextInt(slidingOriginalCacheNames.length);
        String s = slidingOriginalCacheMap.get(slidingOriginalCacheNames[randomInt]);
        return getBase64StrToImage(s);
    }

    /**
     * 随机获取一张 抠图图形的原图
     */
    public static String randomSlidingBlockName(String... ignoreNames) {
        if (null == slidingBlockCacheNames || slidingBlockCacheNames.length == 0)
            return null;

        // 如果有需要排除的文件名 则生成一个新的集合以处理
        if (ignoreNames != null && ignoreNames.length > 0) {
            List<String> newSlidingBlockCacheNames = new ArrayList<>();
            for (String slidingBlockCacheName : slidingBlockCacheNames) {
                boolean flag = true;
                for (String ignoreName : ignoreNames) {
                    if (slidingBlockCacheName.equals(ignoreName)) {
                        flag = false;
                        break;
                    }
                }
                if (flag)
                    newSlidingBlockCacheNames.add(slidingBlockCacheName);
            }
            if (newSlidingBlockCacheNames.isEmpty())
                return null;
            int randomInt = new Random().nextInt(newSlidingBlockCacheNames.size());
            return newSlidingBlockCacheNames.get(randomInt);
        }
        int randomInt = new Random().nextInt(slidingBlockCacheNames.length);
        return slidingBlockCacheNames[randomInt];
    }

    public static BufferedImage getSlidingBlockByName(String fileName) {
        String s = slidingBlockCacheMap.get(fileName);
        return getBase64StrToImage(s);
    }

    /**
     * 随机获取一张汉字点击的 背景原图
     */
    public static BufferedImage randomPicClick() {
        if (null == picClickCacheNames || picClickCacheNames.length == 0) {
            return null;
        }
        int randomInt = new Random().nextInt(picClickCacheNames.length);
        String s = picClickCacheMap.get(picClickCacheNames[randomInt]);
        return getBase64StrToImage(s);
    }

    /**
     * 图片 转 dataUrl
     */
    public static String imageToDataUrl(BufferedImage image) {
        ByteArrayOutputStream oriImagesOs = new ByteArrayOutputStream();//新建流。
        try {
            ImageIO.write(image, "png", oriImagesOs);
            String base64 = Base64.getEncoder().encodeToString(oriImagesOs.toByteArray()).replaceAll("\r|\n", "");
            return "data:image/png;base64," + base64;
        } catch (IOException e) {
            log.error("图片转DataUrl失败！");
        }
        return "";
    }

    /**
     * base64 字符串转图片
     */
    public static BufferedImage getBase64StrToImage(String base64String) {
        try {
            Base64.Decoder decoder = Base64.getDecoder();
            byte[] bytes = decoder.decode(base64String);
            ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
            return ImageIO.read(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 加载图片验证码
     * key: 文件名     value: 文件base64编码
     */
    public static Map<String, String> getResourcesImagesFile(String sourceImgPath) {
        Map<String, String> imgMap = new HashMap<>();

        // 从jar外部文件夹中查询
        File slidingFileDir = new File(FileUtil.getWebRootPath() + File.separator + sourceImgPath);
        if (slidingFileDir.exists()) {
            // 获取所有的图片验证码
            File[] list = slidingFileDir.listFiles();
            if (list == null || list.length < 1)
                throw new BaseException(CommonResultCode.CONSUMER_ERR, "图片验证码为空" + slidingFileDir.getAbsolutePath());
            for (int i = 0; i < list.length; i++) {
                File file = list[i];
                if (!file.getName().endsWith(".png"))
                    continue;
                try (InputStream in = new FileInputStream(list[i])) {
                    byte[] bytes = FileCopyUtils.copyToByteArray(in);
                    String fileBase64 = Base64.getEncoder().encodeToString(bytes).trim();
                    String filename = String.valueOf(i).concat(".png");
                    imgMap.put(filename, fileBase64);
                } catch (IOException e) {
                    log.error("验证码读取失败 " + slidingFileDir.getAbsolutePath(), e);
                }
            }
        }
        // 从jar包中获取验证码
        else {
            try {
                org.springframework.core.io.Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(new DefaultResourceLoader()).getResources("classpath:" + sourceImgPath + "/*.png");
                for (int i = 0; i < resources.length; i++) {
                    try (InputStream in = resources[i].getInputStream()) {
                        byte[] bytes = FileCopyUtils.copyToByteArray(in);
                        String fileBase64 = Base64.getEncoder().encodeToString(bytes).trim();
                        String filename = String.valueOf(i).concat(".png");
                        imgMap.put(filename, fileBase64);
                    } catch (Exception e) {
                        log.error("验证码读取失败", e);
                    }
                }
            } catch (IOException e) {
                log.error("图片验证码源文件读取失败 " + sourceImgPath, e);
                throw new BaseException(CommonResultCode.CONSUMER_ERR, "图片验证码源文件读取失败");
            }
        }
        return imgMap;
    }
    /**
     * 内置轻量缓存 主要应对连续多次校验校验是否锁定的情况 避免瞬间大量请求redis
     */
    private static final Cache<String, Long> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .maximumSize(10000)
            .build();
    public static Long getIncrement(String lockKey) {
        String key = RedisUtil.getPrefix() + FAIL_COUNT_PREFIX + lockKey;
        Long increment = cache.getIfPresent(key);
        if (increment == null) {
            RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, Objects.requireNonNull(redisTemplate.getConnectionFactory()));
            increment = entityIdCounter.get();
            cache.put(key, increment);
        }
        return increment;
    }
    // 判断指定用户名是否需要验证验证码
    public static boolean isVerification(String lockKey) {
        return getIncrement(lockKey) >= showCaptchaCount;
    }
    // 判断指定用户名是否已经被锁定
    public static boolean isLock(String lockKey) {
        return getValidCount(lockKey) <= 0;
    }
    public static long getValidCount(String lockKey) {
        return lockCount - getIncrement(lockKey);
    }
    // 为指定用户名 加锁标记
    public static void lock(String lockKey) {
        String key = RedisUtil.getPrefix() + FAIL_COUNT_PREFIX + lockKey;
        cache.invalidate(key);
        RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, Objects.requireNonNull(redisTemplate.getConnectionFactory()));
        entityIdCounter.getAndIncrement();
        entityIdCounter.expire(lockDuration, TimeUnit.MINUTES);
    }
    // 清除指定用户名的错误次数
    public static void cleanLock(String lockKey) {
        String key = RedisUtil.getPrefix() + FAIL_COUNT_PREFIX + lockKey;
        cache.invalidate(key);
        redisTemplate.delete(key);
    }
}
